# -*- coding:utf-8 -*-

import threading

import os

import time
from collections import OrderedDict

from jinja2 import Environment, PackageLoader
from pandas import Series
from plotly.utils import pandas, numpy
from xlwt import Workbook
from xlrd import open_workbook
from xlutils.copy import copy

from com import MyDevice, LogUtil, ScriptJava, EmailUtil, DrawUtil, BugUtils

_receiver_debug = ['234843654@qq.com']
_receiver = [
    'rom-test@meitu.com',
    'rom-dev@meitu.com',
]
_subject_warn = u'【列表滑动专项】{0}测试报告-【预警】'
_subject_normal = u'【列表滑动专项】{0}测试报告'

# 摘要
_summary = u'【列表滑动专项】【{0}】连续{1}个版本帧耗时超过16.7ms'
# 评论
_comment = u'''
预置条件：
1. 安装{0}应用
2. 填充数据

操作步骤：
1. 打开应用{0}
2. 滑动20次，使用dumpsys gfxinfo获取滑动过程中的帧耗时
3. 重复1-2 10次
4. 取10次的平均值作为本次版本的帧耗时

实际结果：
连续检测最近的{1}个版本，帧耗时为{2} ms

预期结果：
连续测试{1}个版本，帧耗时均小于16.7ms。


'''

# 版本涨幅控制,单位ms
_range = 10

# 版本监控窗口期
_sustained_num = 3

_info_warn = u'以下应用在最近的{0}个版本出现帧率变慢'

_info = u'未发现帧率持续下降应用'

# 限定的最大帧耗时
_limit_ft = 16.7
# 限定的掉帧率
_limit_jr = 0.01

# 换行符
_line_split = '\n'


class FpsPath:
    def __init__(self, is_debug=True):
        self.is_debug = is_debug
        root = '.'
        self.xls_dir = root + '/data/'
        self.sn_all = 'all'
        self.sn_framestats = 'framestats'
        self.html_temp_xls = 'temp.xls'
        self.html_dir = root + '/html/'
        self.html_jank_rate = self.html_dir + 'jank_rate.html'
        self.html_per_rate = self.html_dir + 'per_rate.html'
        if not os.path.exists(self.xls_dir):
            os.makedirs(self.xls_dir)
        if not os.path.exists(self.html_dir):
            os.makedirs(self.html_dir)


# 配合脚本的测试任务
class SlideTask(threading.Thread):
    def __init__(self, device, mpath):
        super(SlideTask, self).__init__()
        assert isinstance(device, MyDevice)
        assert isinstance(mpath, FpsPath)
        self.device = device
        self.mpath = mpath

    def run(self):
        self.start_test()

    def start_test(self):
        # 开启一个线程抓fps数据
        self.start_fps_task()
        # 开始测试
        self.device.i('开始抓fps')
        # 使用autoscript进行滑动
        LogUtil.w('启动流畅性脚本')
        script = ScriptJava(self.device)
        script.for_fluency_test()
        time.sleep(60)

        # 结束测试
        self.stop_fps_task()
        self.device.i('停止抓fps')

        # 获取SurfaceFlingerLatency的解析结果

    def start_fps_task(self):
        self.task = GfxTask(self.device, self.mpath)
        self.task.start()

    def stop_fps_task(self):
        self.task.stop()


class FpsDataUtil:

    # 字符串转换编码
    @classmethod
    def get_decode(cls, s):
        if isinstance(s, str):
            return s.decode('utf8')
        else:
            return s

    @classmethod
    def save_list_to_excel(cls, des_dir, file_name, sheet_name, data_list, append=True):
        assert isinstance(data_list, list)
        file_name = FpsDataUtil.get_decode(file_name)
        sheet_name = FpsDataUtil.get_decode(sheet_name)
        if data_list is None or data_list.__len__() < 1:
            return
        path = des_dir + file_name
        w = Workbook()
        # 判断目录
        if not os.path.exists(des_dir):
            os.makedirs(des_dir)
        # 判断文件
        if not os.path.exists(path):
            w.add_sheet(sheet_name)
            w.save(path)
        rb = open_workbook(path)
        wb = copy(rb)
        sheet_list = rb.sheet_names()
        # 判断sheet
        if sheet_name not in sheet_list:
            wb.add_sheet(sheet_name)
            wb.save(path)
        # 重新获取数据
        rb = open_workbook(path)
        wb = copy(rb)
        sheet_list = rb.sheet_names()
        index = sheet_list.index(sheet_name)
        ws = wb.get_sheet(index)

        # 创建表头
        head_dict = data_list.__getitem__(0)
        if not isinstance(head_dict, dict):  # 转换
            head_dict = head_dict.__dict__
        for i in range(head_dict.keys().__len__()):
            key = head_dict.keys()[i]
            ws.write(0, i, FpsDataUtil.get_decode(key))
        # 追加
        if append:
            row = ws.rows.__len__()
        else:
            row = 1
        for d in data_list:
            if not isinstance(d, dict):
                d = d.__dict__
            for i in range(d.keys().__len__()):
                ws.write(row, i, FpsDataUtil.get_decode(d.values()[i]))
            row += 1
        wb.save(path)

    @classmethod
    def draw_data(cls, mpath):
        import plotly.graph_objs as go
        assert isinstance(mpath, FpsPath)
        # 分组计算
        jank_list = []
        per_list = []
        xls = mpath.html_dir + mpath.html_temp_xls
        sn = mpath.sn_all
        data = pandas.read_excel(xls, sn)
        mgroup = data.groupby(GfxFrame.app_name)
        from pandas.core.groupby import DataFrameGroupBy
        assert isinstance(mgroup, DataFrameGroupBy)
        for g in mgroup:
            group_name = g[0]
            df = g[1]
            assert isinstance(df, pandas.DataFrame)
            jank_rate = go.Scatter(
                x=df[GfxFrame.dev_name],
                y=df[GfxFrame.jank_nums] / df[GfxFrame.f_nums],
                mode='lines',
                # name=GfxFrame.target_list[group_name]
                name=group_name
            )
            per_rate = go.Scatter(
                x=df[GfxFrame.dev_name],
                y=df[GfxFrame.per_95],
                mode='lines',
                # name=GfxFrame.target_list[group_name]
                name=group_name
            )

            jank_list.append(jank_rate)
            per_list.append(per_rate)
        import plotly
        # plotly.offline.plot(jank_list, filename=mpath.html_jank_rate)
        # plotly.offline.plot(per_list, filename=mpath.html_per_rate)
        # jank_plot = DrawUtil.draw_and_get_id(jank_list, 'jank rate')
        per_plot = DrawUtil.draw_and_get_id(per_list, 'frame time')
        return [per_plot]

    @classmethod
    def send_report(cls, plot_id_list, mpath):
        current_version, bug_list = FpsDataUtil.get_bug_list(mpath, sustained_num=_sustained_num)
        env = Environment(loader=PackageLoader('com', 'templates'))
        template = env.get_template('fluency.html')
        # 有bug
        if bug_list.__len__() > 0:
            subject = _subject_warn.format(current_version)
            template = env.get_template('fluency.html')
            info = _info_warn.format(_sustained_num)
            head_list = [u'应用', u'近{0}个版本的帧耗时'.format(_sustained_num), u'bug_id']
            html_msg = template.render(info=info, head_list=head_list, data_list=bug_list,
                                       plot_list=plot_id_list)
        else:
            subject = _subject_normal.format(current_version)
            info = _info
            html_msg = template.render(info=info, plot_list=plot_id_list)
        if mpath.is_debug:
            receiver = _receiver_debug
        else:
            receiver = _receiver
        EmailUtil.send_html(receiver, subject, html_msg)

    @classmethod
    def get_median(cls, cols):
        assert isinstance(cols, Series)
        data = cols.tolist()
        data.sort()
        if data.__len__() > 10:
            # 取中间段的50%的数据作为分析
            start = int(data.__len__() * 0.25)
            end = int(data.__len__() * 0.75)
            data = data[start:end]
        half = len(data) // 2
        median = (data[half] + data[~half]) / 2
        mean = round(numpy.mean(data), 2)
        return mean

    @classmethod
    def parse_histroy_data(cls, mpath, version_num=-1):
        assert isinstance(mpath, FpsPath)
        files = os.listdir(mpath.xls_dir)
        # 按版本号排序
        name = files.__getitem__(0)
        start = name.index('_') + 1
        end = name.index('.xls')
        files.sort(key=lambda x: int(x[start:end]))

        # 截取关注的最近几个版本号
        if version_num > files.__len__() or version_num < 0:
            version_num = files.__len__()
        target_files = files[:version_num]
        m_list = []
        for f in target_files:
            device_name = f.replace('.xls', '')
            path = mpath.xls_dir + f
            data = pandas.read_excel(path)
            mgroup = data.groupby(GfxFrame.app_name)
            for g in mgroup:
                app_name = g[0]
                # 如果进程不是关注进程，则不保存
                if not GfxFrame.target_list.has_key(app_name):
                    print app_name
                    continue

                df = g[1]
                temp = OrderedDict()
                temp[GfxFrame.dev_name] = device_name
                temp[GfxFrame.app_name] = app_name
                assert isinstance(df, pandas.DataFrame)
                for c_name, col in df.iteritems():
                    # 去掉分组列
                    if GfxFrame.app_name == c_name:
                        continue
                    else:
                        LogUtil.i(device_name + ' ' + app_name + ' ' + c_name)
                        temp[c_name] = FpsDataUtil.get_median(col)
                m_list.append(temp)

        # 保存到excle
        mdir = mpath.html_dir
        xls = mpath.html_temp_xls
        if os.path.exists(mdir + xls):
            os.remove(mdir + xls)
        FpsDataUtil.save_list_to_excel(mpath.html_dir, mpath.html_temp_xls, mpath.sn_all, m_list)
        # 画图
        plot_list = FpsDataUtil.draw_data(mpath)
        FpsDataUtil.send_report(plot_list, mpath)

    @classmethod
    def increase_tend(cls, data_list, sustained_num=_sustained_num):
        assert isinstance(data_list, list)
        l = data_list.__len__()
        if sustained_num == -1:
            sustained_num = l
        if l < sustained_num:
            return False
        mlist = data_list[l - sustained_num:l]
        for i in range(1, sustained_num):
            pre = mlist[i - 1]
            current = mlist[i]
            if pre > current:
                return False
        return True

    # 当前版本是否超过规定帧耗时
    @classmethod
    def over_frame_time(cls, data_list):
        return data_list[-1] > _limit_ft

    # 最近的几个版本的数据是否都超过规定帧耗时
    @classmethod
    def keep_over_frame_time(cls, data_list, sustained_num=_sustained_num):
        l = data_list.__len__()
        if sustained_num == -1:
            sustained_num = l
        if l < sustained_num:
            return False
        mlist = data_list[l - sustained_num:l]
        for data in mlist:
            if data < _limit_ft:
                return False
        return True

    @classmethod
    def get_bug_list(cls, mpath, sustained_num):
        assert isinstance(mpath, FpsPath)
        import pandas as pd
        df = pd.read_excel(mpath.html_dir + mpath.html_temp_xls, mpath.sn_all)
        groups = df.groupby(GfxFrame.app_name)
        bug_list = []
        # 判断平均帧耗时
        for g in groups:
            group_name = g[0]
            app_name = GfxFrame.target_list[group_name]
            d_list = g[1][GfxFrame.per_95].tolist()
            v_list = g[1][GfxFrame.dev_name].tolist()
            if sustained_num == -1:
                sustained_num = d_list.__len__()
            if sustained_num > d_list.__len__():
                continue
            # 截取最后的几个版本信息
            data_list = d_list[- sustained_num:]
            version_list = v_list[- sustained_num:]
            current_version = version_list[-1]

            over_ft = FpsDataUtil.over_frame_time(data_list)
            keep_over_ft = FpsDataUtil.keep_over_frame_time(data_list)
            print app_name, data_list, '当前是否越界:', over_ft, '是否持续越界:', keep_over_ft

            # 如果越界，则先警告
            if over_ft:
                # 如果是持续增长，只是预警
                # 如果出现超过60ms的情况，则报bug
                d = OrderedDict()
                d[GfxFrame.app_name] = app_name
                d[GfxFrame.recent_mode] = data_list
                bug_id = 'None'
                # 连续3个版本都越界，则报bug
                if keep_over_ft:
                    summary = _summary.format(app_name, sustained_num)
                    comment = _comment.format(app_name, sustained_num, data_list)
                    bug_id,bug_title = BugUtils.postBug(product=u'自动化专用',
                                              component=u'自动化专用',
                                              bug_severity=BugUtils.S3,
                                              version=current_version,
                                              cf_freq=BugUtils.Freq_a,
                                              cf_isold=BugUtils.Is_new,
                                              cf_hw_version="DVT2",
                                              cf_model="X系统",
                                              summary=summary,
                                              comment=comment,
                                              imei='')
                d[GfxFrame.bug_id] = bug_id
                bug_list.append(d)

        return current_version, bug_list


class GfxFrame:
    app_name = 'Name'
    f_nums = 'Total frames rendered'
    jank_nums = 'Janky frames'
    per_50 = '50th percentile'
    per_90 = '90th percentile'
    per_95 = '95th percentile'
    per_99 = '99th percentile'
    recent_mode = 'recent_mode'
    miss_vsy = 'Number Missed Vsync'
    input_late = 'Number High input latency'
    slow_ui = 'Number Slow UI thread'
    slow_upload = 'Number Slow bitmap uploads'
    slow_draw = 'Number Slow issue draw commands'
    dev_name = 'Device Name'
    bug_id = 'bug_id'
    target_list = {
        'browser': u'浏览器',
        'calculator2': u'计算器',
        'calendar': u'日历',
        'deskclock': u'桌面',
        'dialer': u'拨号',
        # 'launcher3': u'launcher',
        'photos': u'图库',
        'settings': u'设置',
        'mms': u'信息',
        'contacts': u'联系人',
        'notes': u'美图便签',
    }


class GfxTask(threading.Thread):
    __INDEX_PROFILEDATA = '---PROFILEDATA---'
    __INDEX_VISIBLE = 'visibility='
    __CLOS_VISIBLE = 'Visible'
    __CLOS_VIEW = 'View'
    __CLOS_PKG = 'Pkg'

    def __init__(self, device, mpath):
        assert isinstance(mpath, FpsPath)
        super(GfxTask, self).__init__()
        assert isinstance(device, MyDevice)
        self.device = device
        self.__stop_flag = False
        self.mpath = mpath
        self.sheet_all = self.mpath.sn_all
        self.sheet_framestats = self.mpath.sn_framestats
        self.stop_flas = False

    def run(self):
        while not self.stop_flas:
            self.dump_gfx_info()
            time.sleep(1)

    def parse_data(self, line):
        line = line.strip()
        if '(' in line:
            line = line[0:line.index('(')]
        if 'ms' in line:
            line = line[0:line.index('ms')]
        start = line.index(':') + 1
        mdata = line[start:].strip()
        return mdata

    def stop(self):
        self.stop_flas = True

    def parse_app_name(self, pkg):
        start = pkg.rindex('.') + 1
        app_name = pkg[start:]
        return app_name

    @classmethod
    def parse_views_framestats(cls, view_list, profile_list):
        view_framestats_list = []
        assert isinstance(view_list, list)
        assert isinstance(profile_list, list)
        if view_list.__len__() != profile_list.__len__():
            LogUtil.w('数据有误！')
            return None
        for i in range(0, profile_list.__len__()):
            profile = profile_list[i]
            view = view_list[i]
            # 解析profile_data部分
            fstats_list = GfxTask.parse_profile_data(view, profile)
            if fstats_list is not None and fstats_list.__len__() > 0:
                view_framestats_list.extend(fstats_list)
        return view_framestats_list

    @classmethod
    def parse_profile_data(cls, view_line, profile_data):
        mlist = []
        lines = profile_data.split(_line_split)
        head = lines[0].strip().split(',')
        # 删掉最后一个空元素
        head.pop(head.__len__() - 1)
        for i in range(1, lines.__len__()):  # 最后一行为'---PROFILEDATA---\n\n'无无效数据
            if lines[i].strip() == '':
                continue
            temp = lines[i].split(',')
            # 删掉最后一个空元素
            temp.pop(temp.__len__() - 1)
            mdata = OrderedDict()
            vs = view_line.split('/')
            mdata[GfxTask.__CLOS_PKG] = vs[0]
            mdata[GfxTask.__CLOS_VIEW] = vs[1]
            mdata[GfxTask.__CLOS_VISIBLE] = view_line[-2:-1]  # 截取最后一位
            for i in range(0, temp.__len__()):
                mdata[head[i]] = temp[i]
            mlist.append(mdata)
        return mlist

    # 解析统计学数据
    def parse_static_data(self, lines, current_pkg):
        if lines is None or lines.__len__() < 1:
            return None
        frames_data = OrderedDict()
        app_name = self.parse_app_name(current_pkg)
        frames_data[GfxFrame.app_name] = app_name
        for line in lines:
            if GfxFrame.f_nums in line:
                f_nums = int(self.parse_data(line))
                frames_data[GfxFrame.f_nums] = f_nums
                if f_nums is 0:
                    return
            elif GfxFrame.jank_nums in line:
                frames_data[GfxFrame.jank_nums] = int(self.parse_data(line))
            elif GfxFrame.per_50 in line:
                frames_data[GfxFrame.per_50] = int(self.parse_data(line))
            elif GfxFrame.per_90 in line:
                frames_data[GfxFrame.per_90] = int(self.parse_data(line))
            elif GfxFrame.per_95 in line:
                frames_data[GfxFrame.per_95] = int(self.parse_data(line))
            elif GfxFrame.per_99 in line:
                frames_data[GfxFrame.per_99] = int(self.parse_data(line))
            elif GfxFrame.miss_vsy in line:
                frames_data[GfxFrame.miss_vsy] = int(self.parse_data(line))
            elif GfxFrame.input_late in line:
                frames_data[GfxFrame.input_late] = int(self.parse_data(line))
            elif GfxFrame.slow_ui in line:
                frames_data[GfxFrame.slow_ui] = int(self.parse_data(line))
            elif GfxFrame.slow_upload in line:
                frames_data[GfxFrame.slow_upload] = int(self.parse_data(line))
            elif GfxFrame.slow_draw in line:
                frames_data[GfxFrame.slow_draw] = int(self.parse_data(line))
                break
        # 某些弹窗界面是获取不到gfxinfo信息的，这种脏数据要排除掉
        if frames_data.__len__() < 2:
            return None
        return frames_data

    # dump数据并保存
    def dump_gfx_info(self):
        current_pkg = self.device.get_current_pkg()
        if current_pkg is None:
            self.device.i('获取不到pkg信息')
            return
        cmd = self.device.adb_head + ' dumpsys gfxinfo ' + current_pkg + ' framestats'
        r = os.popen(cmd)
        lines = r.readlines()
        if lines is None or lines.__len__() < 1:
            return
        frames_data = OrderedDict()
        app_name = self.parse_app_name(current_pkg)
        frames_data[GfxFrame.app_name] = app_name

        # 提取统计学部分并保存
        static_data = self.parse_static_data(lines, current_pkg)
        if static_data is not None:
            LogUtil.i(static_data)
            xls_name = self.device.get_info() + '.xls'
            FpsDataUtil.save_list_to_excel(self.mpath.xls_dir, xls_name, self.sheet_all, [static_data])

        # 提取profiledata部分
        temp = ''
        i = 0
        profile_section_list = []
        save = False
        for line in lines:
            # 遇到第一个profiledata则开始保存数据
            if GfxTask.__INDEX_PROFILEDATA in line:
                save = True
                i = i + 1
                if i % 2 is 1:
                    continue
            if save:
                x = line.replace(GfxTask.__INDEX_PROFILEDATA, '').replace(_line_split, '')
                temp = temp + x + _line_split
                # 遇到第二个profiledata则停止保存
                if GfxTask.__INDEX_PROFILEDATA in line:
                    save = False
                    profile_section_list.append(temp)
                    temp = ''

        # 提取Views部分
        view_list = []
        for line in lines:
            if GfxTask.__INDEX_VISIBLE in line:
                view_list.append(line.strip())

        if profile_section_list.__len__() < 1 or view_list.__len__() < 1:
            LogUtil.i('未发现PROFILE_DATA或Views')
            return

        # 解析profile_section
        view_framestats_list = GfxTask.parse_views_framestats(view_list, profile_section_list)

        # 保存framestats
        if view_framestats_list is not None and view_framestats_list.__len__() > 0:
            LogUtil.i(view_framestats_list)
            xls_name = self.device.get_info() + '.xls'
            FpsDataUtil.save_list_to_excel(self.mpath.xls_dir, xls_name, self.sheet_framestats, view_framestats_list)

        # 清理framestats
        cmd = self.device.adb_head + ' dumpsys gfxinfo ' + current_pkg + ' reset'
        os.popen(cmd)
